In C or C++, it is possible to provide an initial value for the elements of an array. When fewer values are provided than the size of the array,
the last elements of the array are zero-initialized for builtin-types (like int
or pointers), and value-initialized otherwise.
However, as soon as some values are provided, it is clearer to provide them all and not rely on these default initializations.
int a1[5] = {1, 2, 3}; // Noncompliant, last two elements are initialized with 0
int a2[4] = {1, 2, 3, 4, 5}; // Compliant
int* a3[3] = {a1, a1 + 1}; // Noncompliant, the last pointer is null
int* a4[3] = {a1, a1 + 1, nullptr}; // Compliant
Similarly, when an aggregate class or struct is initialized, an initial value may be provided for each field. All remaining fields are initialized
in the same manner as elements of an array, but this rule requests explicit initialization:
struct Pod {
int x;
int y;
};
Pod p1{1}; // Noncompliant, `y` does not have an initial value
Pod p2{1, 0}; // Compliant
This behavior applies recursively when arrays and aggregates are nested:
struct PodPair {
Pod first;
Pod second;
};
struct ArrayMember {
int id;
int vals[4];
};
int c1[2][2] = {{1}, {2}}; // Noncompliant, the second elements of each nested array do not have an initial value provided
Pod c2[3] = {{1, 2}, {2}}; // Noncompliant, field `y` of `c2[1]` and whole `c2[2]` object do not have an initial value provided
PodPair c3 = {{1}}; // Noncompliant, field `y` of `c3.first` and whole `c3.second` object do not have an initial value provided,
ArrayMember c4 = {1, 2, 3}; // Noncompliant, the last two elements of `c4.vals` do not have an initial value provided
This rule raises an issue when a non-zero initialization of an aggregate (array or class/struct), does not provide values for all its elements or
fields.
What is the potential impact?
The intent of the code is unclear when the initializer omits the values for some of the elements or fields: Is the initial value skipped on purpose
or is it an oversight? Is it because, after the initial code was written, the array size was changed, or a new field was added, and the initialization
was not updated? In that case, the zero or default value may not be handled properly, leading to unexpected program behavior.
As an illustration, if the field or element has a pointer type, it will be initialized with a null-pointer value, and may lead to null-pointer
dereference.
What about zero initialization?
When initializing an array with a large number of elements, or a complex structure, it is often desired to set all the elements or fields to zero,
or to their default values (for classes with default constructors). Such situations are usually indicated by an empty set of braces {}
for C++, or braces with {0}
or {NULL}
in the case of C. The issue is not raised in such a situation.
int a1[10] = {0}; // Compliant
int a2[10] = {}; // Compliant
Pod p1 = {0}; // Compliant
PodPair c1 = {0}; // Compliant
This exception also applies when the nested aggregate is zero-initialized:
int c1[2][2]{{1, 2}, {}}; // Compliant
Pod c2[3] = {{1, 2}, {}, {}}; // Compliant
PodPair c3 = {{}, {1, 0}}; // Compliant
ArrayMember c4{1, {}}; // Compliant
What if designated initializers are used?
The C standard provides a designated initialization syntax that explicitly denotes the field or element of the aggregate for which value is
provided. This rule also raises an issue if the initial value for an element or field is not provided.
int a1[3] = { [1] = 1, [2] = 2 }; // Noncompliant, first element do not have initial value provided
int a2[3] = { [0] = 0, [1] = 1, [2] = 2 }; // Compliant
Pod p1 = {.y = 10}; // Noncompliant, `p.x` do not have initial value provided
Pod p2 = {.x = 0, .y = 10}; // Compliant
C++20 has adopted a limited version of this feature. This rule also raises an issue in the case of incomplete initializers.
What if the field has a default member initializer?
C++11 allows class fields to specify a default value that will be used if no other value is provided during construction. Such default member
initializers cannot be used in aggregate types until C++14, which guarantees that the default value is used if the initializer list does not provide a
value for the field. The issue is not raised when a field with a default value is not explicitly initialized, as an initial value was provided by the
class author, and should be handled properly.
struct Aggr {
int a;
int b;
int c = 0;
};
Aggr a0{}; // Compliant, zero-initialization
Aggr a1{10}; // Noncompliant, no initial value for field `b`, which does not have a default value
Aggr a2{10, 20}; // Compliant, field `c` has a default value specified in `Aggr` definition
Aggr a3{10, 20, 30}; // Compliant, all fields have initial values
With the use of a designated initializer, such default value is meaningful for non-trailing fields:
struct Mid {
int a;
int b = 10;
int c;
};
Mid m1{.a = 10, .b = 10}; // Noncompliant, no initial value for field `c`, which does not have a default value
Mid m1{.a = 10, .c = 2}; // Compliant, field `b` has a default value specified in `Mid` definition
Can the issue be raised if I use parenthesis?
C++20 allows aggregate types to be initialized using the parenthesis (()
) in addition to the braces ({}
), which
simplifies writing generic code that creates an object (see S6872 for more details). This syntax also allows not to specify all values
during initialization and this rule raises an issue in that case too.
int a1[5](1, 2, 3); // Noncompliant, the last two elements do not have an initial value
int a2[4](1, 2, 3, 4, 5); // Compliant
Pod p1(1); // Noncompliant, `y` does not have an inital value
Pod p2(1, 0); // Compliant